import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import {
Play, RotateCcw, RotateCw, Trash2, Code, Layout, Plus, X, Folder,
Settings, Terminal, Sun, Moon, Zap, Clock, Monitor, Tablet, Smartphone,
Download, Upload, Menu, FileText, Trash, AlertTriangle,
PanelRight, PanelTop, GripVertical,
Keyboard,
// --- 新增功能:匯入登入/登出圖示 ---
LogIn, LogOut, User
} from 'lucide-react';
// --- 新增功能:匯入 Firebase ---
import { initializeApp } from "firebase/app";
import {
getAuth,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
signOut
} from "firebase/auth";
import {
getFirestore,
collection,
doc,
setDoc,
deleteDoc,
onSnapshot,
query,
where
} from "firebase/firestore";
// --- 新增功能:使用您提供的 Firebase 設定 ---
const firebaseConfig = {
apiKey: "AIzaSyDBJEKcGNpV4kCt81ziW0Z7zfEVKhpvYL0",
authDomain: "codebox-pro.firebaseapp.com",
projectId: "codebox-pro",
storageBucket: "codebox-pro.firebasestorage.app",
messagingSenderId: "1073960323519",
appId: "1:1073960323519:web:8c36f1d1c1a8a5c645f90a",
measurementId: "G-R9P5HNM7VM"
};
// --- 新增功能:初始化 Firebase ---
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
// --- 預設程式碼內容 (已簡化) ---
const initialContents = {
html: `
範例頁面
歡迎!
這是一個可調整大小的佈局。
`,
css: `/* 基本 CSS 樣式 */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
padding: 20px;
background-color: #fff;
transition: background-color 0.3s;
}
body.dark-mode {
background-color: #222;
color: #eee;
}
h1 {
color: #333;
}
body.dark-mode h1 {
color: #eee;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
`,
js: `// 基本 JavaScript
console.log("腳本已載入!");
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('my-button');
if (button) {
button.addEventListener('click', () => {
console.log('按鈕被點擊了!');
document.body.classList.toggle('dark-mode');
});
}
});
`
};
// --- 專案結構與歷史記錄管理 ---
const MAX_HISTORY = 50;
const createInitialProject = (id, name, content) => ({
id: id || crypto.randomUUID(),
name: name,
fileContents: content,
history: {
html: [content.html],
css: [content.css],
js: [content.js],
},
historyIndex: {
html: 0,
css: 0,
js: 0,
},
// --- 新增功能:確保新專案有 libraries 屬性 ---
libraries: { js: [], css: [] }
});
/**
* 核心 App 組件
*/
const App = () => {
const initialProjects = useMemo(() => [
createInitialProject('p1', '專案 1 (範例)', initialContents)
], []);
// --- 狀態 ---
// --- 新增功能:Firebase 驗證狀態 ---
const [user, setUser] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false); // 追蹤驗證是否已初始化
// --- 修改:移除 localStorage 讀取,改為空陣列 ---
const [projects, setProjects] = useState([]);
// --- 修改:移除 localStorage 讀取 ---
const [activeProjectId, setActiveProjectId] = useState(null);
const [activeTab, setActiveTab] = useState('html');
const [previewSrc, setPreviewSrc] = useState('');
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [autoRunEnabled, setAutoRunEnabled] = useState(true);
const [theme, setTheme] = useState('dark');
const [previewDevice, setPreviewDevice] = useState('desktop');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [alertModal, setAlertModal] = useState({ isOpen: false, message: '' });
const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', message: '', onConfirm: null });
const [consoleLogs, setConsoleLogs] = useState([]);
const [layoutMode, setLayoutMode] = useState('horizontal');
const [mainPaneSize, setMainPaneSize] = useState(50);
const [consolePaneSize, setConsolePaneSize] = useState(160);
const [isResizingMain, setIsResizingMain] = useState(false);
const [isResizingConsole, setIsResizingConsole] = useState(false);
const importFileRef = useRef(null);
const { activeProject, activeProjectIndex } = useMemo(() => {
if (!activeProjectId || projects.length === 0) {
return { activeProject: null, activeProjectIndex: -1 };
}
const index = projects.findIndex(p => p.id === activeProjectId);
if (index === -1) {
// 如果找不到,但有專案,則指向第一個
if(projects.length > 0) {
setActiveProjectId(projects[0].id); // 觸發
return { activeProject: projects[0], activeProjectIndex: 0 };
}
return { activeProject: null, activeProjectIndex: -1 };
}
return {
activeProject: projects[index],
activeProjectIndex: index
};
}, [projects, activeProjectId]);
// --- 專案管理操作 (在 effects 之前定義) ---
const handleNewProject = useCallback(async () => {
const newId = crypto.randomUUID();
const blankContent = {
html: `\n\n\n 新專案\n\n\n 新專案
\n\n`,
css: `/* 新專案 CSS */\nbody {\n padding: 1rem;\n}`,
js: `// 新專案 JS\nconsole.log('新專案已載入');`
};
const newProject = createInitialProject(newId, `新專案 ${projects.length + 1}`, blankContent);
// --- 修改:如果登入,寫入 Firestore ---
if (user) {
try {
const projectDocRef = doc(db, 'userProjects', user.uid, 'projects', newProject.id);
await setDoc(projectDocRef, newProject);
} catch (error) {
console.error("建立 Firestore 專案失敗:", error);
setAlertModal({ isOpen: true, message: "建立雲端專案失敗。" });
}
}
// 本地狀態更新 (Firestore 登入時 onSnapshot 會處理,但登出時需要這個)
if (!user) {
setProjects(prevProjects => [...prevProjects, newProject]);
}
setActiveProjectId(newId);
setActiveTab('html');
setIsSidebarOpen(false);
}, [user, projects.length]); // 依賴 user
// --- 特效 (Effects) ---
// --- 新增功能:Firebase 驗證監聽 ---
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
setIsAuthReady(true);
});
return () => unsubscribe(); // 清理監聽器
}, []);
// --- 新增功能:Firebase 專案讀取 (切換本地/雲端) ---
useEffect(() => {
if (!isAuthReady) return; // 等待驗證初始化
let unsubscribe = () => {};
if (user) {
// --- 登入狀態:從 Firestore 讀取 ---
const projectsColRef = collection(db, 'userProjects', user.uid, 'projects');
const q = query(projectsColRef);
unsubscribe = onSnapshot(q, (snapshot) => {
if (snapshot.empty) {
// 如果 Firestore 沒專案,建立一個新的
handleNewProject();
} else {
const fetchedProjects = snapshot.docs.map(doc => ({
...doc.data(),
id: doc.id,
// 確保舊資料相容
libraries: doc.data().libraries || { js: [], css: [] }
}));
setProjects(fetchedProjects);
// 檢查 activeProjectId 是否有效
const savedId = localStorage.getItem('code-pro-activeProjectId');
if (savedId && fetchedProjects.find(p => p.id === savedId)) {
setActiveProjectId(savedId);
} else {
setActiveProjectId(fetchedProjects[0].id);
}
}
}, (error) => {
console.error("讀取 Firestore 失敗:", error);
setAlertModal({ isOpen: true, message: "讀取雲端專案失敗。" });
});
} else {
// --- 登出狀態:從 localStorage 讀取 ---
try {
const savedProjects = localStorage.getItem('code-pro-projects');
const parsedProjects = savedProjects ? JSON.parse(savedProjects) : [];
if (parsedProjects.length > 0) {
// 確保舊資料相容
setProjects(parsedProjects.map(p => ({
...p,
libraries: p.libraries || { js: [], css: [] }
})));
} else {
setProjects(initialProjects);
}
const savedId = localStorage.getItem('code-pro-activeProjectId');
if (savedId && parsedProjects.find(p => p.id === savedId)) {
setActiveProjectId(savedId);
} else {
setActiveProjectId(parsedProjects.length > 0 ? parsedProjects[0].id : initialProjects[0].id);
}
} catch (e) {
console.error("Failed to load projects from localStorage", e);
setProjects(initialProjects);
setActiveProjectId(initialProjects[0].id);
}
}
return () => unsubscribe(); // 清理 onSnapshot 監聽器
}, [user, isAuthReady, handleNewProject, initialProjects]); // 依賴 user 和 isAuthReady
// --- 修改:localStorage 儲存 (僅在登出時) ---
useEffect(() => {
if (isAuthReady && !user && projects.length > 0) {
try {
localStorage.setItem('code-pro-projects', JSON.stringify(projects));
} catch (e) {
console.error("Failed to save projects to localStorage", e);
}
}
}, [projects, user, isAuthReady]);
// --- 修改:activeProjectId 儲存 (這個可以一直存在 localStorage) ---
useEffect(() => {
if (activeProjectId) {
localStorage.setItem('code-pro-activeProjectId', activeProjectId);
}
}, [activeProjectId]);
// 主題切換
useEffect(() => {
const root = window.document.documentElement;
if (theme === 'light') root.classList.remove('dark');
else root.classList.add('dark');
}, [theme]);
// 監聽來自 iframe 的 console log
useEffect(() => {
const handleConsoleMessage = (event) => {
if (event.data && event.data.type && event.data.type.startsWith('console-')) {
setConsoleLogs(prevLogs => [
...prevLogs.slice(-100),
{
type: event.data.type,
message: event.data.message,
timestamp: new Date().toLocaleTimeString('en-US', { hour12: false })
}
]);
}
};
window.addEventListener('message', handleConsoleMessage);
return () => window.removeEventListener('message', handleConsoleMessage);
}, []);
// 處理面板大小調整
const handleMouseMove = useCallback((e) => {
e.preventDefault();
if (isResizingMain) {
if (layoutMode === 'horizontal') {
const newSize = (e.clientX / window.innerWidth) * 100;
setMainPaneSize(Math.max(20, Math.min(80, newSize)));
} else {
const newSize = (e.clientY / window.innerHeight) * 100;
setMainPaneSize(Math.max(20, Math.min(80, newSize)));
}
}
if (isResizingConsole) {
const newHeight = window.innerHeight - e.clientY;
setConsolePaneSize(Math.max(40, Math.min(window.innerHeight * 0.6, newHeight)));
}
}, [isResizingMain, isResizingConsole, layoutMode]);
const handleMouseUp = useCallback(() => {
setIsResizingMain(false);
setIsResizingConsole(false);
}, []);
useEffect(() => {
if (isResizingMain || isResizingConsole) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
} else {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}, [isResizingMain, isResizingConsole, handleMouseMove, handleMouseUp]);
// --- 核心功能:執行/更新預覽 ---
const updatePreview = useCallback(() => {
if (!activeProject) return;
setConsoleLogs([]);
// --- 修改:納入 libraries (如果有的話) ---
const { fileContents, libraries } = activeProject;
const { html, css, js } = fileContents;
const safeLibraries = libraries || { js: [], css: [] };
const cssLinks = (safeLibraries.css || []).map(url => ``).join('\n');
const jsScripts = (safeLibraries.js || []).map(url => ``).join('\n');
const fullCSS = ``;
const consoleInterceptor = `
`;
const userScript = ``;
let finalHTML = html;
if (finalHTML.includes('')) {
finalHTML = finalHTML.replace('', `\n${consoleInterceptor}\n${cssLinks}`);
} else {
finalHTML = `${consoleInterceptor}\n${cssLinks}\n${finalHTML}`;
}
if (finalHTML.includes('')) {
finalHTML = finalHTML.replace('', `${fullCSS}\n`);
} else {
finalHTML = `${fullCSS}\n${finalHTML}`;
}
if (finalHTML.includes('')[0] + '\n\n\n